/**
 * Copyright (C) 2013 DaiKit.com - daikit4gxt module (admin@daikit.com)
 *
 *         Project home : http://code.daikit.com/daikit4gxt
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.daikit.daikit4gxt.client.action;

import com.daikit.commons.shared.utils.DkStringUtils;
import com.daikit.daikit4gxt.client.DkMain;
import com.daikit.daikit4gxt.client.action.ActionChainExecution.ExecutionStatus;
import com.daikit.daikit4gxt.client.log.BaseLogger;
import com.google.gwt.i18n.client.NumberFormat;
import com.google.gwt.user.client.Timer;


/**
 * 
 * 
 * <br>
 * It is forbidden (and not possible) to run 2 chains in parallel so be careful not to start a new chain of actions
 * while already executing one.
 * 
 * @author tcaselli
 * @version $Revision$ Last modifier: $Author$ Last commit: $Date$
 * @param <T>
 *           the action result type
 */
public abstract class BaseAction<T>
{

	protected static final BaseLogger LOG = BaseLogger.getLog(BaseAction.class);

	protected final NumberFormat timeFormat = NumberFormat.getFormat("#,##0");
	protected final NumberFormat stepFormat = NumberFormat.getFormat("#00");

	private String chainActionExecutionId;
	private boolean showProgress;
	private boolean enableTimeout;

	protected String progressMessage;

	private BaseAction<?> chainAction;
	private BaseAction<?> parentChainAction;
	private BaseAction<?> nextExecutionAction;
	private BaseAction<?> nextExecutionChainAction;

	protected ChainActionCreator chainActionCreator;

	private T result; // used for next action to be able to retrieve previous action returned value.

	protected abstract void run();

	/**
	 * Wrapper to {@link #BaseAction(String, boolean)} with showProgress=true
	 * 
	 * @param progressMessage
	 *           the progress message
	 */
	public BaseAction(final String progressMessage)
	{
		this(progressMessage, true);
	}

	/**
	 * Be careful with the showProgress flag. If this action is contained in a chain showing progress setting the
	 * showProgress flag to false will lead to a ProgressDialog that can't get finished. Use only the showProgress flag =
	 * false if you are creating a not chained action or within a chain with all actions with showProgress flag = false.<br>
	 * <br>
	 * 
	 * An action with a chain Action will ignore its chainActionCreator even if not null.
	 * 
	 * <br>
	 * <br>
	 * !!! Do not interlace paging actions. Be sure to have finished loading one paging before starting another. !!!
	 * 
	 * @param progressMessage
	 *           the progress message
	 * @param showProgress
	 *           boolean indicating whether to display the progress bar step for this action
	 */
	public BaseAction(final String progressMessage, final boolean showProgress)
	{
		enableTimeout = true;
		chainAction = null;
		this.progressMessage = progressMessage;
		this.showProgress = showProgress;
	}

	/**
	 * @return whether a chain of actions is currently running. It is forbidden (and not possible) to run 2 chains in
	 *         parallel so be careful not to start a new chain of actions while already executing one.
	 */
	public static final boolean isChainRunning()
	{
		return ActionChainExecution.get().isChainRunning();
	}

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// PUBLIC METHODS / EXECUTION
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	/**
	 * Execute the action chain. Wrapper to {@link #execute(boolean)} with false argument
	 * 
	 * @return an {@link ExecutionStatus}.
	 */
	public final ExecutionStatus execute()
	{
		return execute(false);
	}

	/**
	 * Execute the action chain. Use the given parameter with caution, by default it is false when calling directly
	 * {@link #execute()} method. If the current chain action has already a last chain action creator there will be an
	 * error.
	 * 
	 * @param createChainActionCreatorIfAlreadyRunning
	 *           a boolean indicating whether the action to be started has to be added in a chain action creator set at
	 *           the end of currently running chain if the action to be started is not part of the currently running
	 *           chain
	 * 
	 * @return an {@link ExecutionStatus}.
	 */
	public final ExecutionStatus execute(final boolean createChainActionCreatorIfAlreadyRunning)
	{
		final ExecutionStatus status = ActionChainExecution.get().onActionStarted(this, createChainActionCreatorIfAlreadyRunning);

		if (!ExecutionStatus.CANCELED.equals(status) && !ExecutionStatus.ERROR_1.equals(status)
				&& !ExecutionStatus.ERROR_2.equals(status) && !ExecutionStatus.ERROR_3.equals(status))
		{
			if (ExecutionStatus.POSTPONED.equals(status))
			{
				ActionChainExecution.get().getRunningAction().getLastChainAction().setChainActionCreator(new ChainActionCreator()
				{
					@Override
					public BaseAction<?> getCreatedActions()
					{
						return BaseAction.this;
					}
				}, false);
			}
			else
			{
				if (ExecutionStatus.CHAIN_STARTING.equals(status))
				{
					final String numberOfSteps = stepFormat.format(ActionChainExecution.get().getNumberOfSteps());
					final String stepName = debugName();
					LOG.debug(DkStringUtils.format("[CMD] === [START] === CHAIN ACTION -- nbSteps=[%s] -- firstStep=[%s]",
							numberOfSteps, stepName));
					BaseActionProgressDialog.get().close();
				}

				if (isShowProgress())
				{
					BaseActionProgressDialog.get().setEnableTimeout(enableTimeout);
					BaseActionProgressDialog.get().increment(this, progressMessage);
				}

				if (LOG.isDebugEnabled())
				{
					final String incrementIndex = String.valueOf(stepFormat.format(ActionChainExecution.get().getIncrementIndex()));
					final String numberOfSteps = stepFormat.format(ActionChainExecution.get().getNumberOfSteps());
					final String stepName = debugName();
					LOG.debug(DkStringUtils.format("[CMD] --- [START STEP] - index=[%s/%s] - step=[%s]", incrementIndex,
							numberOfSteps, stepName));
				}

				// Time to launch the run method of the action
				try
				{
					final Timer timer = new Timer()
					{
						@Override
						public void run()
						{
							try
							{
								BaseAction.this.run();
							}
							catch (final Throwable t)
							{
								onClientActionRunningException(t);
							}
						}
					};
					// This time is useful to let the time for the progress dialog to be updated before running the run method.
					// This because the progress dialog send an update event when we change its progress message ... and this
					// event is postponed after the run method by the JavaScript execution.
					timer.schedule(DkMain.config().getActionDelayBeforeRunMilliseconds());
				}
				catch (final Throwable t)
				{
					onClientActionRunningException(t);
				}
			}
		}
		return status;
	}

	/**
	 * An error happened during action run execution.
	 * 
	 * @param t
	 *           the exception
	 */
	public void onClientActionRunningException(final Throwable t)
	{
		if (this instanceof HasProxyAsyncCallback)
		{
			LOG.error(DkStringUtils.format("ERROR IN ACTION : [%s]", debugName()), t);
			((HasProxyAsyncCallback) this).getProxyAsyncCallback().onFailure(t);
		}
		DkMain.errorHandler().handle(this, t);
		onFailure(t);
	}

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// PUBLIC METHODS / PROCESS SUCCESS AND FAILURE
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	/**
	 * Callback method after a failure happened.0
	 * 
	 * @param caught
	 *           the exception
	 */
	public void onFailure(final Throwable caught)
	{
		ActionChainExecution.get().endChainExecution();
	}

	/**
	 * Wrapper to {@link #onSuccess(Object)} with null argument.
	 */
	public void onSuccess()
	{
		onSuccess(null);
	}

	/**
	 * 
	 * @param result
	 */
	public void onSuccess(final T result)
	{
		setResult(result);
		if (ActionChainExecution.get().isChainStopped())
		{
			ActionChainExecution.get().endChainExecution();
			LOG.debug(DkStringUtils.format("[CMD] === [END - STOPPED] === CHAIN ACTION Execution took [%s] [msec]",
					timeFormat.format(System.currentTimeMillis() - ActionChainExecution.get().getChainStartTime())));
			return;
		}
		if (ActionChainExecution.get().isChainPaused())
		{
			LOG.debug(DkStringUtils.format("[CMD] === [CHAIN PAUSED] === CHAIN ACTION was paused."));
			return;
		}
		if (ActionChainExecution.get().isChainTimeout())
		{
			LOG.debug(DkStringUtils.format(
					"Action step=[%s] took=[%s] [msec] - timeout=[%s] [msec] was successful but ignored because of timeout.",
					debugName(),
					timeFormat.format(System.currentTimeMillis() - ActionChainExecution.get().getCurrentActionStartTime()),
					timeFormat.format(DkMain.config().getActionTimeoutMilliseconds())));
			return;
		}
		final Timer timer = new Timer()
		{
			@Override
			public void run()
			{
				// don't execute chain action creator if a chain action is set.
				if (getChainAction() == null && chainActionCreator != null)
				{
					nextExecutionAction = chainActionCreator.getCreatedActions();
					chainActionCreator = null;
					if (nextExecutionAction != null)
					{
						nextExecutionAction.setLastChainAction(nextExecutionChainAction);
					}
				}

				if (LOG.isDebugEnabled())
				{
					final String incrementIndex = stepFormat.format(ActionChainExecution.get().getIncrementIndex());
					final String numberOfSteps = stepFormat.format(ActionChainExecution.get().getNumberOfSteps());
					final String ignoredSteps = String.valueOf(BaseActionProgressDialog.get().getNbStepToIgnore());
					final String tookMs = String.valueOf(timeFormat.format(System.currentTimeMillis()
							- ActionChainExecution.get().getCurrentActionStartTime()));
					final String stepName = debugName();
					final String nextStepName = getChainAction() != null ? getChainAction().debugName()
							: nextExecutionAction != null ? nextExecutionAction.debugName() : "NO NEXT STEP";
					LOG.debug(DkStringUtils.format(
							"[CMD] --- [END   STEP] - index=[%s/%s] - ign=[%s] - took=[%s] [msec] - step=[%s] - next step=[%s]",
							incrementIndex, numberOfSteps, ignoredSteps, tookMs, stepName, nextStepName));
				}

				if (isShowProgress())
				{
					BaseActionProgressDialog.get().endStep(0);
				}

				if (getChainAction() != null)
				{
					getChainAction().execute();
				}
				else
				{
					final long endTimeMs = System.currentTimeMillis();
					if (ActionChainExecution.get().getIncrementIndex() > 0
							&& ActionChainExecution.get().getIncrementIndex() < ActionChainExecution.get().getNumberOfSteps())
					{
						LOG.debug("[CMD] --- [INFO] - CHAIN ACTION was cut by a chain action creator.");
					}
					ActionChainExecution.get().endChainExecution();
					LOG.debug(DkStringUtils.format("[CMD] === [END] === CHAIN ACTION Execution took [%s] [msec]",
							timeFormat.format(endTimeMs - ActionChainExecution.get().getChainStartTime())));
					if (nextExecutionAction != null)
					{
						LOG.debug("[CMD] --- [INFO] --- CHAIN ACTION created from chain action creator.");
						nextExecutionAction.execute();
					}
				}
			}
		};
		timer.schedule(DkMain.config().getActionDelayBeforeNewtActionExecutionMilliseconds());
	}

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// PUBLIC METHODS / CHAIN OPERATIONS AT RUNTIME.
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	/**
	 * Pause a chain (it can be resumed by {@link #resumeChain(Object)}). The progress bar will be paused but remains
	 * visible (if shown)
	 */
	public void pauseChain()
	{
		ActionChainExecution.get().setChainPaused(true);
	}

	/**
	 * Stop the chain. The progress bar will be closed.
	 */
	public void stopChain()
	{
		ActionChainExecution.get().setChainStopped(true);
	}

	/**
	 * The action went in timeout.
	 */
	public void stopBecauseOfTimeout()
	{
		ActionChainExecution.get().setChainTimeout(true);
		ActionChainExecution.get().endChainExecution();
		LOG.debug(DkStringUtils.format("[CMD] === [END - ACTION TIMEOUT] === CHAIN ACTION Execution took [%s] [msec]",
				timeFormat.format(System.currentTimeMillis() - ActionChainExecution.get().getChainStartTime())));
	}

	/**
	 * Resume a chain (after being paused by {@link #pauseChain()}).
	 * 
	 * @param result
	 *           the result of the action.
	 */
	public void resumeChain(final T result)
	{
		if (ActionChainExecution.get().isChainStopped())
		{
			LOG.error("[CMD] ====== [ERROR] === A Stopped Chain cannot be resumed. Use pauseChain() instead.");
			return;
		}
		if (!ActionChainExecution.get().isChainPaused())
		{
			LOG.error("[CMD] ====== [ERROR] === A not paused Chain cannot be resumed.");
		}
		LOG.debug(DkStringUtils.format("[CMD] === [CHAIN RESUMED] === CHAIN ACTION was resumed after a pause."));
		ActionChainExecution.get().setChainPaused(false);
		onSuccess(result);
	}

	/**
	 * @return the previously executed action result.
	 */
	@SuppressWarnings("unchecked")
	public <X> X getPreviousActionResult()
	{
		if (getParentChainAction() == null)
		{
			return null;
		}
		return (X) getParentChainAction().getResult();
	}

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// PUBLIC METHODS / UTILS
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	/**
	 * @return the debug name of this action (by default it is the progress message)
	 */
	public String debugName()
	{
		return progressMessage;
	}

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// PUBLIC METHODS ## ADD/GET CHAIN ACTION & CHAIN ACTION CREATOR
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	/**
	 * Set a {@link ChainActionCreator} in the last action of this action chain. Be careful not to add any chain action
	 * to this action afterwards because it will prevent the given ChainActionCreator to be executed.<br>
	 * You can add last chain actions to the actions chain created by the given ChainActionCreator with method :
	 * {@link #setNextExecutionLastChainAction(BaseAction)}
	 * 
	 * @param chainActionCreator
	 *           the {@link ChainActionCreator} to be added
	 */
	public void setLastChainActionCreator(final ChainActionCreator chainActionCreator)
	{
		getLastChainAction().setChainActionCreator(chainActionCreator, false);
	}

	/**
	 * Add a {@link ChainActionCreator} to this action. <br>
	 * If you set addThisChainActionsAsNextExecutionLastChainActions to false, be careful that this action has no chain
	 * action because they will prevent the ChainActionCreator to be executed.<br>
	 * If you set addThisChainActionsAsNextExecutionLastChainActions to true, this action chain actions will be removed
	 * from this chain action and will be added to the action chain created by the given ChainActionCreator
	 * 
	 * @param chainActionCreator
	 *           the {@link ChainActionCreator} to be added
	 * @param addThisChainActionsAsNextExecutionLastChainActions
	 */
	public void setChainActionCreator(final ChainActionCreator chainActionCreator,
			final boolean addThisChainActionsAsNextExecutionLastChainActions)
	{
		if (this.chainActionCreator != null)
		{
			LOG.warn(DkStringUtils.format("Setting a chain action creator in an action [%s] which already has one. ",
					this.debugName()));
		}
		if (addThisChainActionsAsNextExecutionLastChainActions)
		{
			setNextExecutionLastChainAction(this.getChainAction());
			this.setChainAction(null);
		}
		else if (getChainAction() != null)
		{
			LOG.warn(DkStringUtils
					.format(
							"Setting a ChainActionCreator in action : [%s] but this action has already a chain action : [%s]. So the given ChainActionCreator won't be executed.",
							this.debugName(), chainAction.debugName()));
		}
		this.chainActionCreator = chainActionCreator;
	}

	/**
	 * Set the chain action. If
	 * 
	 * @param chainAction
	 */
	protected void setChainAction(final BaseAction<?> chainAction)
	{
		if (chainAction != null)
		{
			if (getChainAction() != null)
			{
				LOG.warn(DkStringUtils
						.format(
								"Be careful, you are adding a chain action [%] to this action [%s] which already has a chain action [%]. So the already existing chain action will be overridden.",
								chainAction.debugName(), this.debugName(), this.getChainAction().debugName()));
			}
			this.chainAction = chainAction;
			if (chainActionCreator != null)
			{
				LOG.warn(DkStringUtils
						.format(
								"Be careful, you are adding a chain action [%s] to this action [%s] which already has a ChainActionCreator. It will prevent this ChainActionCreator to be executed.",
								chainAction.debugName(), this.debugName()));
				LOG.warn("You can add last chain actions to the actions chain that will be created by the current action ChainActionCreator with method : setNextExecutionLastChainAction(BaseAction)}");
			}
			chainAction.setParentChainAction(this);
		}
	}

	/**
	 * @return the last action of this chain of actions.
	 */
	public BaseAction<?> getLastChainAction()
	{
		BaseAction<?> currentChainAction = chainAction;
		if (currentChainAction == null)
		{
			return this;
		}
		while (currentChainAction.getChainAction() != null)
		{
			currentChainAction = currentChainAction.getChainAction();
		}
		return currentChainAction;
	}

	/**
	 * Adds the given action to the end of the current nextExecutionChainAction. This nextExecutionChainAction action
	 * chain will be chained to the action created by this {@link ChainActionCreator} (if any)
	 * 
	 * @param chainAction
	 *           the chain action to be added to the nextExecutionChainAction
	 */
	public void setNextExecutionLastChainAction(final BaseAction<?> chainAction)
	{
		if (nextExecutionChainAction == null)
		{
			nextExecutionChainAction = chainAction;
		}
		else
		{
			nextExecutionChainAction.getLastChainAction().setChainAction(chainAction);
		}
	}

	/**
	 * Add an action to the end of this chain of actions.
	 * 
	 * @param chainAction
	 *           the {@link BaseAction} to be added.
	 */
	public void setLastChainAction(final BaseAction<?> chainAction)
	{
		if (this.equals(chainAction))
		{
			LOG.error("Setting chain action equal to this. -> will lead to non terminable loop. Setting ignored");
		}
		else
		{
			getLastChainAction().setChainAction(chainAction);
		}
	}

	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
	// GETTERS / SETTERS
	// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

	/**
	 * @return the progressMessage
	 */
	public String getProgressMessage()
	{
		return progressMessage;
	}

	/**
	 * @return the chainStopped
	 */
	public boolean isChainStopped()
	{
		return ActionChainExecution.get().isChainStopped();
	}

	/**
	 * @return the chainTimeout
	 */
	public boolean isChainTimeout()
	{
		return ActionChainExecution.get().isChainTimeout();
	}

	/**
	 * @return the chainPaused
	 */
	public boolean isChainPaused()
	{
		return ActionChainExecution.get().isChainPaused();
	}

	/**
	 * @return the result
	 */
	public T getResult()
	{
		return result;
	}

	/**
	 * @param result
	 *           the result to set
	 */
	public void setResult(final T result)
	{
		this.result = result;
	}

	/**
	 * @return the parentChainAction
	 */
	public BaseAction<?> getParentChainAction()
	{
		return parentChainAction;
	}

	/**
	 * @param parentChainAction
	 *           the parentChainAction to set
	 */
	public void setParentChainAction(final BaseAction<?> parentChainAction)
	{
		this.parentChainAction = parentChainAction;
	}

	/**
	 * @return the showProgress
	 */
	public boolean isShowProgress()
	{
		return showProgress;
	}

	/**
	 * @param showProgress
	 *           the showProgress to set
	 */
	public void setShowProgress(final boolean showProgress)
	{
		this.showProgress = showProgress;
	}

	/**
	 * @return the enableTimeout
	 */
	public boolean isEnableTimeout()
	{
		return enableTimeout;
	}

	/**
	 * @param enableTimeout
	 *           the enableTimeout to set
	 */
	public void setEnableTimeout(final boolean enableTimeout)
	{
		this.enableTimeout = enableTimeout;
	}

	/**
	 * @return the chainAction
	 */
	public BaseAction<?> getChainAction()
	{
		return chainAction;
	}

	/**
	 * @return the chainActionExecutionId
	 */
	public String getChainActionExecutionId()
	{
		return chainActionExecutionId;
	}

	/**
	 * @param chainActionExecutionId
	 *           the chainActionExecutionId to set
	 */
	public void setChainActionExecutionId(final String chainActionExecutionId)
	{
		this.chainActionExecutionId = chainActionExecutionId;
	}
}
